package com.ruoyi.business.modbus.serial.jserialcomm;

import com.fazecast.jSerialComm.SerialPort;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;

/**
 * 基于jSerialComm的串口输出流实现
 * 
 * @author ruoyi
 * @date 2024-01-01
 */
public class JSerialCommOutputStream extends OutputStream {
    
    private static final Logger log = LoggerFactory.getLogger(JSerialCommOutputStream.class);
    
    private final SerialPort serialPort;
    private boolean closed = false;
    
    /**
     * 构造函数
     * 
     * @param serialPort jSerialComm串口对象
     */
    public JSerialCommOutputStream(SerialPort serialPort) {
        this.serialPort = serialPort;
        log.debug("创建jSerialComm串口输出流");
    }
    
    @Override
    public void write(int b) throws IOException {
        checkClosed();
        
        byte[] buffer = {(byte) b};
        int bytesWritten = serialPort.writeBytes(buffer, 1);
        
        if (bytesWritten != 1) {
            throw new IOException("写入单字节失败，期望写入1字节，实际写入" + bytesWritten + "字节");
        }
        
        log.trace("写入单字节: 0x{}", Integer.toHexString(b & 0xFF));
    }
    
    @Override
    public void write(byte[] b) throws IOException {
        write(b, 0, b.length);
    }
    
    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        checkClosed();
        
        if (b == null) {
            throw new NullPointerException("缓冲区不能为null");
        }
        if (off < 0 || len < 0 || len > b.length - off) {
            throw new IndexOutOfBoundsException("偏移量或长度参数无效");
        }
        if (len == 0) {
            return;
        }
        
        // 如果需要偏移，创建新的字节数组
        byte[] dataToWrite;
        if (off == 0 && len == b.length) {
            dataToWrite = b;
        } else {
            dataToWrite = new byte[len];
            System.arraycopy(b, off, dataToWrite, 0, len);
        }
        
        int bytesWritten = serialPort.writeBytes(dataToWrite, len);
        
        if (bytesWritten != len) {
            throw new IOException("写入数据不完整，期望写入" + len + "字节，实际写入" + bytesWritten + "字节");
        }
        
        log.trace("写入 {} 字节数据: {}", len, bytesToHex(dataToWrite));
    }
    
    @Override
    public void flush() throws IOException {
        checkClosed();
        
        // jSerialComm的flushIOBuffers方法会清空输入输出缓冲区
        // 这里我们等待输出缓冲区清空
        long startTime = System.currentTimeMillis();
        int maxWait = 1000; // 最大等待1秒
        
        while (System.currentTimeMillis() - startTime < maxWait) {
            int bytesAwaitingWrite = serialPort.bytesAwaitingWrite();
            if (bytesAwaitingWrite == 0) {
                break;
            }
            
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
        
        log.trace("输出缓冲区已刷新");
    }
    
    @Override
    public void close() throws IOException {
        if (!closed) {
            try {
                flush(); // 关闭前先刷新缓冲区
            } catch (IOException e) {
                log.warn("关闭前刷新缓冲区失败: {}", e.getMessage());
            }
            closed = true;
            log.debug("jSerialComm串口输出流已关闭");
        }
    }
    
    /**
     * 检查流是否已关闭
     */
    private void checkClosed() throws IOException {
        if (closed) {
            throw new IOException("输出流已关闭");
        }
        if (serialPort == null || !serialPort.isOpen()) {
            throw new IOException("串口未打开");
        }
    }
    
    /**
     * 写入数据并等待完成
     * 
     * @param data 要写入的数据
     * @param timeoutMs 超时时间(毫秒)
     * @return 实际写入的字节数
     * @throws IOException IO异常
     */
    public int writeAndWait(byte[] data, int timeoutMs) throws IOException {
        checkClosed();
        
        if (data == null || data.length == 0) {
            return 0;
        }
        
        int bytesWritten = serialPort.writeBytes(data, data.length);
        
        if (bytesWritten > 0) {
            // 等待数据发送完成
            long startTime = System.currentTimeMillis();
            
            while (System.currentTimeMillis() - startTime < timeoutMs) {
                int bytesAwaitingWrite = serialPort.bytesAwaitingWrite();
                if (bytesAwaitingWrite == 0) {
                    break;
                }
                
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
        }
        
        log.trace("写入并等待完成: {} 字节", bytesWritten);
        return bytesWritten;
    }
    
    /**
     * 获取等待写入的字节数
     * 
     * @return 等待写入的字节数
     */
    public int getBytesAwaitingWrite() throws IOException {
        checkClosed();
        return serialPort.bytesAwaitingWrite();
    }
    
    /**
     * 清空输出缓冲区
     */
    public void flushOutput() throws IOException {
        checkClosed();
        
        // 清空输出缓冲区中未发送的数据
        serialPort.flushIOBuffers();
        log.debug("已清空输出缓冲区");
    }
    
    /**
     * 发送中断信号
     * 
     * @param duration 持续时间(毫秒)
     */
    public void sendBreak(int duration) throws IOException {
        checkClosed();
        
        serialPort.setBreak();
        
        try {
            Thread.sleep(duration);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } finally {
            serialPort.clearBreak();
        }
        
        log.debug("发送中断信号，持续时间: {}ms", duration);
    }
    
    /**
     * 将字节数组转换为十六进制字符串
     */
    private String bytesToHex(byte[] bytes) {
        if (bytes == null || bytes.length == 0) {
            return "";
        }
        
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < Math.min(bytes.length, 16); i++) { // 最多显示16字节
            if (i > 0) sb.append(" ");
            sb.append(String.format("%02X", bytes[i] & 0xFF));
        }
        if (bytes.length > 16) {
            sb.append("...");
        }
        return sb.toString();
    }
}
